package pl.jad.database.mapper;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.security.InvalidParameterException;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.logging.Logger;

import pl.jad.database.DbConfig;
import pl.jad.database.DbManager;
import pl.jad.database.table.Table;

/**
 * Pozwala na zapisywanie i odczytywanie obiektow korzystajac z bazy danych 
 * jako nosnika danych
 */
final public class DbMapper {
	
	private static Logger _Log = Logger.getLogger(DbMapper.class.getClass().getName());
	
	private Connection _connection;
	
	private static ArrayList<DbMapper> _instances = new ArrayList<DbMapper>();
	
	private DbConfig _config;
	
	// TODO: identity map, remember object was read from db
	
	/**
	 * Tworzy mapper uzywajacy okreslonych ustawien dotyczacych dostepu 
	 * do bazy danych
	 * 
	 * @throws Exception jesli nie uda sie nawiazac polaczenia z BD 
	 */
	protected DbMapper(DbConfig config) throws Exception {
		_config = config;
		_connection = DbManager.getConnection(config);
	}
	
	/**
	 * Zwraca odpowienia instancje tej klasy. 
	 * 
	 * Dba o to aby istnial tylko jeden obiekt dla kazdej konfiguracji polaczenia.
	 * 
	 * @param config
	 * @return
	 * @throws Exception
	 */
	public static DbMapper factory(DbConfig config) throws Exception {
		DbMapper retMapper;
		
		for (DbMapper mapper : _instances) {
			if (mapper.getConnectionConfig().equals(config)) {
				return mapper;
			}
		}
		
		retMapper = new DbMapper(config);
		_instances.add(retMapper);
		return retMapper;
	}
	
	/**
	 * Zwraca polaczenie uzywane przez ten mapper
	 * 
	 * @return polaczenie uzywane przez ten mapper
	 */
	public Connection getConnection() {
		return _connection;
	}
	
	/**
	 * Zwraca ustawienia z jakimi zostalo nawiazane polaczenie z BD
	 * 
	 * @return
	 */
	public DbConfig getConnectionConfig() {
		return _config;
	}
	
	/**
	 * Zapisuje obiekt w BD
	 * 
	 * @param object 		obiekt, ktory ma byc zapisany w bazie
	 * @throws Exception 	jesli wystapi problem podczas zapisywania
	 */
	public int save(Object object) throws Exception {
		if (object == null) {
			throw new NullPointerException("Object not specified");
		}
		
		// TODO: zapisywac tylko zlozone obiekty?
		
		Table table = prepareTable(object);
		 
		insert(table, object);
		return table.getLastIndex();
	}
	
	/**
	 * Przygotowuje tabele w ktorej bede zapisywane obiekty okreslonego typu
	 * 
	 * @param object		obiekt reprezentujacy obiekty, ktore beda w przyszlosci 
	 * zapisywane w BD
	 * @return				tabela w ktorej moga byc zapisywane obiekty
	 * @throws Exception	jesli wystapi blad podczas tworzenia tabeli
	 */
	public Table prepareTable(Object object) throws Exception {
		Table table = new Table(this, object);
		
		if (!hasTable(table)) {
			// TODO: check if table has all required columns,etc.
			createTable(table);
		} else {
			alterTable(table);
		}
		
		return table;
	}
	
	/**
	 * Zapisuje podany obiekt w okreslonej tabeli
	 * 
	 * @param object		obiekt do zapisania w tabeli
	 * @param table			tabela w ktorej zapisac obiekt
	 * @return				unikalne id zapisanego obiektu
	 * @throws Exception	jesli wystapi blad podczas zapisywania obiektu
	 */
	public int put(Object object, Table table) throws Exception {
		insert(table, object);
		return table.getLastIndex();
	}
	
	/**
	 * Sprawdza czy okreslona tabela istnieje juz w bazie danych
	 * 
	 * @param table
	 * @return TRUE jesli tabela istnieje; FALSE jesli tabela nie istnieje
	 * @throws Exception jesli wystapi blad podczas sprawdzania
	 */
	protected boolean hasTable(Table table) throws Exception {
		while (_connection.getMetaData().getTables(null, DbManager.getSchema()
				, table.getName(), null).next()) {
			return true;
		}
		return false;
	}
	
	/**
	 * Usuwa okreslona tabele z bazy danych
	 * 
	 * @param table			tabela ktora usunac z BD
	 * @throws Exception 	jesli wystapi blad podczas usuwania
	 */
	protected void deleteTable(Table table) throws Exception {
		if (!hasTable(table)) {
			// don't have to do anything
		} else {
			executeStatement("DROP TABLE IF EXISTS " + table.getName());
			
			_Log.info(table.getName() + " table deleted");
		}
	}
	
	/**
	 * Tworzy okreslona tabele w BD
	 * 
	 * @param table
	 * @throws SQLException jesli wystapi blad podczas tworzenia tabeli
	 */
	protected void createTable(Table table) throws SQLException {
		executeStatement(table.getCreateSql());
		
		_Log.info(table.getName() + " table created");
	}
	
	/**
	 * Dodaje brakujace kolumny do tabeli - uaktualnia jej budowe
	 * @param table
	 */
	protected void alterTable(Table table) {
		List<String> sqls = table.getAlterColumnsSql();
		for(String sql : sqls) {
			try {
				executeStatement(sql);
				_Log.info("OK - " + sql);
			} catch (Exception e) {
				_Log.warning("FAIL - " + sql);
			}
		}
		
		_Log.info("Table altered");
	}
	
	/**
	 * Dodaje do tabeli informacje o obiekcie
	 * 
	 * @param table
	 * @param object
	 * @throws Exception 
	 */
	protected void insert(Table table, Object object) throws Exception {
		String sql = table.getInsertSql(object);
		executeStatement(sql);
		
		_Log.info("Executed: " + sql);
	}
	
	/**
	 * Pobiera z BD obiekt(y) okreslonej klasy z okreslonym unikalnym indeksem
	 * @param Clas
	 * @param pk
	 * @return
	 * @throws Exception
	 */
	public ArrayList<Object> get(Class<?> Clas, int pk) throws Exception {
		isSupported(Clas);
		
		Table table = new Table(this, Clas);
		ArrayList<Object> objects = new ArrayList<Object>();
		
		if (!hasTable(table)) {
			throw new IllegalArgumentException("No objects stored for the class: " 
					+ Clas.getName());
		} else {
			
			Statement st = _connection.createStatement();
			ResultSet rs = st.executeQuery(table.getSql(pk));
			while (rs.next()) {
				objects.add(readObject(Clas, rs));
				_Log.info(Clas.getName() + " object read from DB");
			}
			
			if (objects.isEmpty()) {
				throw new InvalidParameterException("Object with specified index does " +
						"NOT exist");
			}
		}
		
		return objects;
	}
	
	/**
	 * Zwraca jeden obiekt danej klasy odczytany z bazy danych
	 * 
	 * @param Clas 			klasa obiektu, ktory odczytac
	 * @param pk			indeks obiektu
	 * @return
	 * @throws Exception	jesli nie znaleziono obiektu z podanym indeksem
	 */
	public Object getOne(Class<?> Clas, int pk) throws Exception {
		ArrayList<Object> list = get(Clas, pk);
		if (list.isEmpty()) {
			throw new Exception("Object with specified primary key does not exist: " + pk);
		}
		return list.get(0);
	}
	
	/**
	 * Pobiera z BD obiekty o okreslonych wartosciach podanych pol
	 * 
	 * @param Clas
	 * @param pks
	 * @return
	 * @throws Exception
	 */
	public ArrayList<Object> get(Class<?> Clas, HashMap<String, String> pks) throws Exception {
		isSupported(Clas);
		
		Table table = new Table(this, Clas);
		ArrayList<Object> objects = new ArrayList<Object>();
		
		if (!hasTable(table)) {
			throw new IllegalArgumentException("No objects stored for the class: " 
					+ Clas.getName());
		} else {
			
			Statement st = _connection.createStatement();
			ResultSet rs = st.executeQuery(table.getSql(pks));
			while (rs.next()) {
				objects.add(readObject(Clas, rs));
				_Log.info(Clas.getName() + " object read from DB");
			}
			
			if (objects.isEmpty()) {
				throw new InvalidParameterException("Object with specified index does " +
						"NOT exist");
			}
		}
		
		return objects;
	}
	
	/**
	 * Zwraca z BD wszystkie obiekty danej klasy
	 * 
	 * @param Clas
	 * @return
	 * @throws Exception
	 */
	public ArrayList<Object> getAll(Class<?> Clas) throws Exception {
		isSupported(Clas);
		
		Table table = new Table(this, Clas);
		ArrayList<Object> objects = new ArrayList<Object>();
		
		if (!hasTable(table)) {
			throw new IllegalArgumentException("No objects stored for the class: " 
					+ Clas.getName());
		} else {
			
			Statement st = _connection.createStatement();
			ResultSet rs = st.executeQuery(table.getAllSql());
			while (rs.next()) {
				objects.add(readObject(Clas, rs));
				
				_Log.info(Clas.getName() + " object read from DB");
			}
		}
		
		_Log.info(objects.size() + " objects were read from DB");
		
		return objects;
	}
	
	/**
	 * Sprawdza czy mapper obsluguje zapis danego typu obiektow
	 * 
	 * @param Clas
	 * @throws IllegalArgumentException
	 */
	protected void isSupported(Class<?> Clas) throws IllegalArgumentException {
		if (Table.isPrimitive(Clas)) {
			throw new IllegalArgumentException("You can not store/read primitive type objects");
		}
	}
	
	/**
	 * Zwraca obiekt danej klasy z okreslonego wiersza
	 * 
	 * @param Clas
	 * @param rs
	 * @return
	 */
	protected Object readObject(Class<?> Clas, ResultSet rs) {
		Object object;
		try {
			Constructor<?> constructor = Clas.getConstructor();
			object = constructor.newInstance();
		} catch (Exception e) {
			throw new IllegalArgumentException("You can read only objects with public, " +
			"default constructor. " + e.getMessage());
		}
		
		Field[] fields = Clas.getDeclaredFields();
		for (Field field : fields) {
			try {
				if (Table.isPrimitive(field.getType())) {
					// typ podstawowy
					if (field.getType() == boolean.class) {
						// zamiana 0/1 na false/true
						Boolean B = new Boolean(rs.getObject(field.getName()).toString());
						field.set(object, B.booleanValue());
					}
					field.set(object, rs.getObject(field.getName()));
				} else if (field.getType().isArray()) {
					// tablica
					ArrayList<Object> arrayObjects = getAll(field.getType().getComponentType());
					_Log.info("Odczytano tablice z tyloma obiektami: " + arrayObjects.size());
				} else {
					// obiekt
					int pk = rs.getInt(field.getName());
					Object subObject = get(field.getType(), pk).get(0);
					field.set(object, subObject);
				}
				
				_Log.info(field.getName() + " set to " + field.get(object).toString());
			} catch (Exception e) {
				_Log.info(field.getName() + " could NOT be set");
			}
		}
		
		return object;
	}
	
	/**
	 * Wykonuje okreslone zapytanie SQL
	 * 
	 * @param sql
	 * @throws SQLException
	 */
	protected void executeStatement(String sql) throws SQLException {
		Statement stmt = _connection.createStatement();
		_Log.info(sql);
		stmt.execute(sql);
		stmt.close();
	}
}
